package serverside;

//import
import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ConnectException;
//import java.net.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketTimeoutException;
//
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.util.logging.Level;
import java.util.logging.Logger;
import sun.misc.*;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.SecretKeySpec;
import javax.crypto.SecretKey;
import javax.crypto.KeyGenerator;

/**
 *
 * @author AbdallaEssam
 */
public class MyServer extends Thread {

    /**
     * ********** what is New ******* Add new constructor that accept an
     * integer parameter_port_number_ and check the IllegalArgumentException.
     *
     */
    /**
     * ******* Reporting problems and probable exceptions 1- if any kind of
     * exception/errors is caught in "connect" method the server will stop
     * because there is a break in the catch clause. 2- what is the correct
     * thing to do when there is no place for a new client? 3- what is the write
     * action to perform when an IOexception is caught in "initializeStreams"
     * method and "sendDataToClient" method 4- "receiveDataFromClient" will
     * return "" if a general exception is caught.. we have to eliminate the
     * general catch exception and put specific ones.
     *
     */
    ServerSocket welcomeSocket;
    private static final int maxClientsCount = 10;
    private static final clientThread[] threads = new clientThread[maxClientsCount];
    DataOutputStream os;
    int i;

    public MyServer() throws IOException {
        welcomeSocket = new ServerSocket(6789);
    }

    public MyServer(int port_number) throws IOException {
        try {
            welcomeSocket = new ServerSocket(port_number);
        } catch (IllegalArgumentException ex) {
            //port parameter is outside the specified range of 
            //valid port values, which is between 0 and 65535, inclusive.            
            welcomeSocket = new ServerSocket(6789);
        }
    }

    public void connect() {

        //System.out.println(welcomeSocket.getLocalSocketAddress());
        while (true) {
            try {
                Socket connectionSocket = welcomeSocket.accept();
                i = 0;
                for (i = 0; i < maxClientsCount; i++) {
                    if (threads[i] == null) {
                        (threads[i] = new clientThread(connectionSocket, threads, i)).start();
                        System.out.println("client " + connectionSocket.getInetAddress() + " is connected");
                        break;
                    }
                }
                if (i == maxClientsCount) {
                    DataOutputStream os = new DataOutputStream(connectionSocket.getOutputStream());
                    /**
                     * We need to reconfigure the client to this part or it
                     * change the way it behave by making the client wait until
                     * any place become free
                     */
                    os.writeBytes("Server too busy. Try later.");
                    os.close();
                    System.out.println("Client " + connectionSocket.getInetAddress()
                            + "is forced to leave because of trafic");
                    connectionSocket.close();
                }

            }//try
            catch (IOException e) {
                System.err.println("An Exception occure in connect function");
                //e.printStackTrace();
                break;
            }
        } //end while 
    }//end run

    private static class clientThread extends Thread {

        //class members 
        Socket clientConnectionSocket;
        String clientSentence = "null";
        BufferedReader inFromClient = null;
        DataOutputStream outToClient = null;
        clientThread[] myThreads;
        String transactionResult;
        int index;
        ATM myATM = new ATM();
        int i = 0;
        int j = 0;
        String[] list;
        private static final byte[] keyValue = new byte[]{'T', 'h', 'e', 'B', 'e', 's', 't', 'S', 'e', 'c', 'r', 'e', 't', 'K', 'e', 'y'};

        public clientThread(Socket _connectionSocket, clientThread[] _threads, int _index) {
            clientConnectionSocket = _connectionSocket;
            myThreads = _threads;
            index = _index;
            initializeStreams();
        }//end of constructor 

        private static Key generateKey() throws Exception {
            Key key = new SecretKeySpec(keyValue, "AES");
            return key;
        }

        private static String[] seprate(String x) {
            int temp = 0;
            String[] str = new String[4];
            for (int i = 0; i < 4; i++) {
                str[i] = "";
            }
            for (char ch : x.toCharArray()) {

                if (ch == ',') {
                    temp++;
                    continue;
                }
                str[temp] = str[temp] + ch;
            }
            return str;
        }//end method seprate

        private void initializeStreams() {
            try {
                inFromClient = new BufferedReader(
                        new InputStreamReader(clientConnectionSocket.getInputStream()));
                outToClient = new DataOutputStream(clientConnectionSocket.getOutputStream());
            } catch (IOException ex) {
                //if an exception occure then there will be some errors with the
                // input and/or output stream
                ex.printStackTrace();
            }
        }//end method initializeStreams

        /**
         *
         * @param dataToSend the data you want to send throw the socket
         */
        private void sendDataToClient(String dataToSend) {
            try {
                //outToClient.writeBytes(Encry(dataToSend) + "\n");
                outToClient.writeBytes(dataToSend + "\n");
            } catch (IOException ex) {
                //if an exception occure then something happen when data was
                //being sent
                
                ex.printStackTrace();
            
            }
        }//end method sendDataToClient

        /**
         *
         * @return data received from client or -1 if some exception occur
         */
        private String receiveDataFromClient() {
            try {
                //return Decrypt(inFromClient.readLine());
                return inFromClient.readLine();
            } catch (IOException ex) {
                //some exception occure when receiving data
                return "";
            } catch (Exception ex) {
                Logger.getLogger(MyServer.class.getName()).log(Level.SEVERE, null, ex);
                return "";
            }
        }

        // Encryption technique .
        public static String Encry(String value) {
            try {

                Key key = generateKey();

                Cipher desCipher;

                // Create the cipher
                desCipher = Cipher.getInstance("AES");

                // Initialize the cipher for encryption
                desCipher.init(Cipher.ENCRYPT_MODE, key);

                //sensitive information
                byte[] text = desCipher.doFinal(value.getBytes());

                //System.out.println("Text [Byte Format] : " + text);
                //System.out.println("Text : " + new String(text));
                // Encrypt the text
                String Encrypted = new BASE64Encoder().encode(text);

                return Encrypted;

            } catch (NoSuchAlgorithmException e) {
                e.printStackTrace();
                return "";
            } catch (NoSuchPaddingException e) {
                e.printStackTrace();
                return "";
            } catch (InvalidKeyException e) {
                e.printStackTrace();
                return "";
            } catch (IllegalBlockSizeException e) {
                e.printStackTrace();
                return "";
            } catch (BadPaddingException e) {
                e.printStackTrace();
                return "";
            } catch (Exception ex) {
                Logger.getLogger(MyServer.class.getName()).log(Level.SEVERE, null, ex);
            }
            return null;

        }

        //decryption function
        private static String Decrypt(String encryptedData) throws Exception {
            Key key = generateKey();

            Cipher c = Cipher.getInstance("AES");
            c.init(Cipher.DECRYPT_MODE, key);

            byte[] decordedValue = new BASE64Decoder().decodeBuffer(encryptedData);
            byte[] decValue = c.doFinal(decordedValue);

            String decryptedValue = new String(decValue);
            return decryptedValue;
        }

        public void run() {
            try {
                sendDataToClient("You are connected to ATM server");

                clientSentence = receiveDataFromClient(); 
                System.out.println("ATM ID: " + clientSentence);
                // tell the client that he is connected to server                 

                clientSentence = receiveDataFromClient();

                System.out.println("client " + clientConnectionSocket.getInetAddress()
                        + " sent: " + clientSentence);

                System.err.println("Entering While then switch\n");
                while (!clientSentence.equalsIgnoreCase("EXIT")) {
                    j++;
                    System.err.println("While loop no. " + j);
                    //System.out.println(clientSentence);
                    switch (clientSentence) {
                        case "LOGIN": {
                            sendDataToClient("You can send password and PIN number.");
                            //may be if i check what comes from the next line, the 
                            //NullPointerException dissapear
                            clientSentence = receiveDataFromClient();
                            /**
                             * I have to check clientSentence if it is NULL and
                             * know what to do then
                             */
                            list = seprate(clientSentence);
                            System.out.println(list.toString());

                            myATM.authenticateUser(list[0], list[1]);
                            if (myATM.isUserAuthenticated()) {
                                sendDataToClient("true");
                                System.out.println("Sent True");
                            } else {
                                sendDataToClient("false");
                                System.out.println("Sent False");
                            }
                            break;
                        }
                        case "WITHDRAW": {
                            //if(myATM.isUserAuthenticated())
                            sendDataToClient("You can withdraw.");
                            clientSentence = receiveDataFromClient();
                            System.out.println("FastCash: user sent: " + clientSentence);
                            transactionResult = myATM.fastCash(clientSentence);
                            sendDataToClient(transactionResult);
                            break;
                        }

                        case "FUNDTRANSFER": {
                            //prompt the client to send transaction data
                            //which is account number  and amount
                            sendDataToClient("You can transfer.");
                            // get account number and amount from user in one
                            //string and seperate them
                            clientSentence = receiveDataFromClient();
                            System.out.println("TRANSFER: user sent: " + clientSentence);
                            list = seprate(clientSentence);
                            transactionResult = myATM.Transfer(list[0], list[1]);

                            sendDataToClient(transactionResult);
                            break;
                        }
                        case "BALANCEINQUIRY": {
                            //outToClient.writeBytes("You can query.\n");
                            /**
                             * I have to check if the client wait to receive
                             * "please wait." or "You can query."
                             */
                            sendDataToClient("You can query.");
                            transactionResult = myATM.balanceInquiry();
                            System.err.println("BALANCEINQUIRY: result is " + transactionResult);
                            sendDataToClient(transactionResult);
                            break;
                        }

                        case "BILLPAYMENT": {
                            sendDataToClient("You can pay.");
                            // clientSentence will contain 4 arguments 
                            clientSentence = inFromClient.readLine();
                            System.out.println("BILLPAYMENT: user sent: " + clientSentence);
                            list = seprate(clientSentence);
                            transactionResult = myATM.payBill(list[0], list[1], list[2], list[3]);
                            sendDataToClient(transactionResult);
                            break;
                        }

                        case "CASHDEPOSIT": {
                            sendDataToClient("You can deposit.");
                            clientSentence = inFromClient.readLine();
                            System.out.println("CASHDEPOSIT: user sent: " + clientSentence);
                            transactionResult = myATM.Deposit(clientSentence);
                            sendDataToClient(transactionResult);
                            break;
                        }
                        case "CHANGEPIN": {
                            sendDataToClient("You can change.");
                            clientSentence = receiveDataFromClient();
                            transactionResult = myATM.changePIN(clientSentence);
                            sendDataToClient(transactionResult);
                            break;
                        }
                        default: {
                        }
                    }//end switch    

                    System.out.println("Waiting for next input from client... \n");

                    clientSentence = receiveDataFromClient();
                    System.out.println("client " + clientConnectionSocket.getInetAddress()
                            + " sent: " + clientSentence);
                    if (clientSentence.isEmpty()) {
                        break;
                    }
                }//end while

                inFromClient.close();
                outToClient.close();
                System.out.println("Client " + clientConnectionSocket.getInetAddress()
                        + "is leaving!");
                clientConnectionSocket.close();
                if (myThreads[index] != null) {
                    myThreads[index] = null;
                }
            } catch (SocketTimeoutException e) {
                System.out.println("Socket Timeout Exception\n");
                e.printStackTrace();
            } catch (NullPointerException e) {
//                try {
//                    //System.err.println("NullPointerException occure\n");
//                    //System.err.println("Be ready for input again, but please input write things\n");
//                    
//                    inFromClient.close();
//                    outToClient.close();
//                    System.out.println("Client " + clientConnectionSocket.getInetAddress()
//                            + "is leaving!");
//                    clientConnectionSocket.close();
//                    if (myThreads[index] != null) {
//                        myThreads[index] = null;
//                    }
//                } catch (IOException ex) {
//                    System.err.println("Error inside the catch of nullPointerException");
//                }
            } catch (Exception e) {
                System.out.println("Exception occure\n");
                e.printStackTrace();
                try {
                    inFromClient.close();
                    outToClient.close();
                    System.out.println("Client " + clientConnectionSocket.getInetAddress()
                            + "is leaving!");
                    clientConnectionSocket.close();
                    if (myThreads[index] != null) {
                        myThreads[index] = null;
                    }
                } catch (IOException ex) {
                    System.err.println("Error inside the catch Exceptions");
                }
            }

        }
    }
}
